package com.xzzz.irda.auth.core;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.xzzz.irda.auth.core.annotation.AuthIgnore;
import com.xzzz.irda.auth.core.constants.AuthConstant;
import com.xzzz.irda.auth.core.constants.GrantTypeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 读取配置文件类型
 * <p>
 * 注入方式
 * {com.xzzz.irda.auth.server.EnableAuthServer}
 * {com.xzzz.irda.auth.client.EnableAuthClient}
 *
 * @author wangyf
 * 
 */
@Data
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "xzzz.auth")
public class AuthBaseProperties implements ImportAware, EnvironmentAware, ApplicationContextAware {

    /**
     * 是否开启DEBUG模式
     */
    private Boolean debugEnable = Boolean.FALSE;
    /**
     * 是否显示请求日志
     */
    private Boolean showRequestUri = Boolean.FALSE;
    /**
     * 显示请求细节信息
     */
    private Boolean showRequestDetail = Boolean.FALSE;
    /**
     * 配置需要打印请求参数日志的请求path的list对象
     */
    private List<String> showRequestLogPath;
    /**
     * 动态全局密码
     */
    private String dynamicGlobalPwd = "";
    /**
     * 标识是否是服务方
     */
    private boolean isServer;
    /**
     * 授权平台配置,不同的平台类型生成的 token 时效是不同的
     * <p>
     * 该类创建完成后会检查是否配置平台数据, 未自定义平台配置会创建默认配置
     *
     * @see AuthBaseProperties#initAfterProcessorClient()
     */
    private List<Client> clients;
    /**
     * platforms 转为 map, 方便查询
     */
    private Map<String, Client> clientMap;
    /**
     * 白名单列表
     * 配置时不需要增加context-path,会自动拼接
     */
    private List<String> whiteList;

    public Client getClient(String client) {
        return clientMap.get(client);
    }

    // ------------------------------< 平台类型配置 >------------------------------

    /**
     * 平台类型, 平台类型控制着授权的类型和授权业务字段的显示
     * 平台类型如配置文件不配置, 则按默认配置 {@link Client#getDefault()}
     * 如配置文件中配置:
     * <pre>{@code
     * auth:
     *   client:
     *     - clientId: abc
     *       expire: 14400
     *       requestRefresh: true
     * }</pre>
     */
    @Data
    public static class Client {
        /**
         * 客户端ID
         */
        private String clientId;
        /**
         * 默认过期时间, 默认为 4 小时, {@link Client#getDefault()}
         */
        private Long duration;
        /**
         * 每次请求刷新, 默认为 true, {@link Client#getDefault()}
         */
        private Boolean requestRefresh;
        /**
         * 允许多地登录: 为 true 则每次登录返回的 token 是一样的, 为 false 则每次登录 token 会替换前一个 token, 默认为 true, {@link Client#getDefault()}
         */
        private Boolean multiPlaceLogin;
        /**
         * 该平台允许的登录方式, 默认为 PASSWORD, {@link Client#getDefault()}
         */
        private List<String> grantType;

        public Client() {
        }

        /**
         * 获取默认平台配置
         *
         * @return 平台配置
         * @see AuthBaseProperties#initAfterProcessorClient
         */
        public static Client getDefault() {
            Client client = new Client();
            client.setDuration(60 * 60 * 4L);
            client.setRequestRefresh(Boolean.TRUE);
            client.setMultiPlaceLogin(Boolean.TRUE);
            client.setGrantType(CollUtil.newArrayList(GrantTypeEnum.PASSWORD.getType()));
            return client;
        }
    }


    // ------------------------------< 生命周期控制 >------------------------------

    /**
     * 日志管理
     */
    @Autowired
    private LoggingSystem loggingSystem;

    /**
     * 设置 DEBUG 模式, 获取「客户端」与「服务端」启用注解的 DEBUG 参数
     *
     * @param annotationMetadata 注解元数据
     */
    @Override
    public void setImportMetadata(AnnotationMetadata annotationMetadata) {

        // 获取服务端注解
        Map<String, Object> serverAttributesMap =
                annotationMetadata.getAnnotationAttributes(AuthConstant.SERVER_ANNOTATION_NAME, true);
        AnnotationAttributes attrs = AnnotationAttributes.fromMap(serverAttributesMap);

        isServer = serverAttributesMap != null;

        // 获取不到则获取客户端注解
        if (null == attrs) {
            this.debugEnable = false;
            Map<String, Object> clientAttributesMap =
                    annotationMetadata.getAnnotationAttributes(AuthConstant.CLIENT_ANNOTATION_NAME, true);
            attrs = AnnotationAttributes.fromMap(clientAttributesMap);
        }

        if (null != attrs) {
            this.debugEnable = attrs.getBoolean("debug");
            if (this.debugEnable) {
                // 设置路径日志级别为debug，否则注解开启后仍需修改配置文件日志级别才可显示DEBUG日志
                loggingSystem.setLogLevel("com.xzzz.irda.auth", LogLevel.DEBUG);
            }
        }

    }

    @PostConstruct
    public void init() {
        // 白名单后置处理
        initAfterProcessorWhiteList();
        // 平台配置后置处理
        initAfterProcessorClient();
    }

    /**
     * 白名单后置处理
     */
    private void initAfterProcessorWhiteList() {
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        Map<RequestMappingInfo, HandlerMethod> methodMap = mapping.getHandlerMethods();
        methodMap.forEach((key, value) -> {
            if (null != AnnotationUtil.getAnnotation(value.getMethod(), AuthIgnore.class)) {
                if (key.getPatternsCondition() != null) {
                    Set<String> urls = key.getPatternsCondition().getPatterns();
                    whiteList.addAll(urls);
                } else if (key.getPathPatternsCondition() != null) {
                    Set<PathPattern> patterns = key.getPathPatternsCondition().getPatterns();
                    if (CollUtil.isNotEmpty(patterns)) {
                        Set<String> urls = patterns.stream().map(PathPattern::getPatternString).collect(Collectors.toSet());
                        whiteList.addAll(urls);
                    }
                }
            }
        });

        String contentPath = env.getProperty(AuthConstant.SERVLET_CONTEXT_PATH);
        // 设置默认登陆页白名单
        if (CollUtil.isEmpty(whiteList)) {
            this.whiteList = new ArrayList<>();
        }
        whiteList.addAll(AuthConstant.DEFAULT_WHITE_LIST);
        this.whiteList = this.whiteList.stream()
                .map(s -> Optional.ofNullable(contentPath).orElse("") + s)
                .collect(Collectors.toList());
    }

    /**
     * 平台配置后置处理,设置默认平台类型
     */
    private void initAfterProcessorClient() {
        // 初始化客户端列表
        if (CollUtil.isEmpty(this.clients)) {
            this.clientMap = new HashMap<>();
            this.clients = new ArrayList<>();
        }

        if (!isServer) {
            return;
        }

        final Client defaultClient = Client.getDefault();

        log.info("[AUTHORIZ] 平台授权配置信息, Client:客户端, Expire(h):授权时间(小时), Refresh:请求刷新授权, ManyPlace:多地登录");
        log.info("[AUTHORIZ] ┌───────────────┬───────────┬─────────┬────────────┐");
        log.info("[AUTHORIZ] | ClientId      | Expire(h) | Refresh | MultiPlace |");
        log.info("[AUTHORIZ] ├───────────────┼───────────┼─────────┼────────────┤");
        for (Client client : this.clients) {
            // 配置文件有配置,但配置不全,则未配置的参数使用默认配置
            if (client.getDuration() == null || client.getDuration() == 0) {
                client.setDuration(defaultClient.getDuration());
            }
            if (client.getRequestRefresh() == null) {
                client.setRequestRefresh(defaultClient.getRequestRefresh());
            }
            if (client.getMultiPlaceLogin() == null) {
                client.setMultiPlaceLogin(defaultClient.getMultiPlaceLogin());
            }
            if (CollUtil.isEmpty(client.getGrantType())) {
                client.setGrantType(defaultClient.getGrantType());
            }
            log.info("[AUTHORIZ] | {}| {}| {}| {}| {}",
                    StrUtil.fillAfter(client.getClientId(), StrUtil.C_SPACE, 14),
                    StrUtil.fillAfter(client.getDuration() / 3600L + "", StrUtil.C_SPACE, 10),
                    StrUtil.fillAfter(String.valueOf(client.getRequestRefresh()), StrUtil.C_SPACE, 8),
                    StrUtil.fillAfter(String.valueOf(client.getMultiPlaceLogin()), StrUtil.C_SPACE, 11),
                    client.getGrantType()
            );
        }
        // 转换为 map, 有相同的 type, 后者会覆盖前者
        this.clientMap = this.clients.stream().collect(Collectors.toMap(Client::getClientId, Function.identity(), (key1, key2) -> key2));
        log.info("[AUTHORIZ] └───────────────┴───────────┴─────────┴────────────┘");
    }

    /**
     * 环境配置
     */
    private Environment env;

    @Override
    public void setEnvironment(Environment environment) {
        this.env = environment;
    }

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
